<template>
  <BaseContentPlaceholders v-if="contentLoading">
    <BaseContentPlaceholdersBox
      :rounded="true"
      class="w-full"
      style="height: 40px"
    />
  </BaseContentPlaceholders>
  <div
    v-else
    :id="id"
    ref="multiselect"
    :tabindex="tabindex"
    :class="classList.container"
    @focusin="activate"
    @focusout="deactivate"
    @keydown="handleKeydown"
    @focus="handleFocus"
  >
    <!-- Search -->
    <template v-if="mode !== 'tags' && searchable && !disabled">
      <input
        ref="input"
        :type="inputType"
        :modelValue="search"
        :value="search"
        :class="classList.search"
        :autocomplete="autocomplete"
        @input="handleSearchInput"
        @paste.stop="handlePaste"
      />
    </template>

    <!-- Tags (with search) -->
    <template v-if="mode == 'tags'">
      <div :class="classList.tags">
        <slot
          v-for="(option, i, key) in iv"
          name="tag"
          :option="option"
          :handleTagRemove="handleTagRemove"
          :disabled="disabled"
        >
          <span :key="key" :class="classList.tag">
            {{ option[label] }}
            <span
              v-if="!disabled"
              :class="classList.tagRemove"
              @mousedown.stop="handleTagRemove(option, $event)"
            >
              <span :class="classList.tagRemoveIcon"></span>
            </span>
          </span>
        </slot>

        <div :class="classList.tagsSearchWrapper">
          <!-- Used for measuring search width -->
          <span :class="classList.tagsSearchCopy">{{ search }}</span>

          <!-- Actual search input -->
          <input
            v-if="searchable && !disabled"
            ref="input"
            :type="inputType"
            :modelValue="search"
            :value="search"
            :class="classList.tagsSearch"
            :autocomplete="autocomplete"
            style="box-shadow: none !important"
            @input="handleSearchInput"
            @paste.stop="handlePaste"
          />
        </div>
      </div>
    </template>

    <!-- Single label -->
    <template v-if="mode == 'single' && hasSelected && !search && iv">
      <slot name="singlelabel" :value="iv">
        <div :class="classList.singleLabel">
          {{ iv[label] }}
        </div>
      </slot>
    </template>

    <!-- Multiple label -->
    <template v-if="mode == 'multiple' && hasSelected && !search">
      <slot name="multiplelabel" :values="iv">
        <div :class="classList.multipleLabel">
          {{ multipleLabelText }}
        </div>
      </slot>
    </template>

    <!-- Placeholder -->
    <template v-if="placeholder && !hasSelected && !search">
      <slot name="placeholder">
        <div :class="classList.placeholder">
          {{ placeholder }}
        </div>
      </slot>
    </template>

    <!-- Spinner -->
    <slot v-if="busy" name="spinner">
      <span :class="classList.spinner"></span>
    </slot>

    <!-- Clear -->
    <slot
      v-if="hasSelected && !disabled && canClear && !busy"
      name="clear"
      :clear="clear"
    >
      <span :class="classList.clear" @mousedown="clear"
        ><span :class="classList.clearIcon"></span
      ></span>
    </slot>

    <!-- Caret -->
    <slot v-if="caret" name="caret">
      <span
        :class="classList.caret"
        @mousedown.prevent.stop="handleCaretClick"
      ></span>
    </slot>

    <!-- Options -->
    <div :class="classList.dropdown" tabindex="-1">
      <div class="w-full overflow-y-auto">
        <slot name="beforelist" :options="fo"></slot>

        <ul :class="classList.options">
          <template v-if="groups">
            <li
              v-for="(group, i, key) in fg"
              :key="key"
              :class="classList.group"
            >
              <div
                :class="classList.groupLabel(group)"
                :data-pointed="isPointed(group)"
                @mouseenter="setPointer(group)"
                @click="handleGroupClick(group)"
              >
                <slot name="grouplabel" :group="group">
                  <span>{{ group[groupLabel] }}</span>
                </slot>
              </div>

              <ul :class="classList.groupOptions">
                <li
                  v-for="(option, i, key) in group.__VISIBLE__"
                  :key="key"
                  :class="classList.option(option, group)"
                  :data-pointed="isPointed(option)"
                  @mouseenter="setPointer(option)"
                  @click="handleOptionClick(option)"
                >
                  <slot name="option" :option="option" :search="search">
                    <span>{{ option[label] }}</span>
                  </slot>
                </li>
              </ul>
            </li>
          </template>
          <template v-else>
            <li
              v-for="(option, i, key) in fo"
              :key="key"
              :class="classList.option(option)"
              :data-pointed="isPointed(option)"
              @mouseenter="setPointer(option)"
              @click="handleOptionClick(option)"
            >
              <slot name="option" :option="option" :search="search">
                <span>{{ option[label] }}</span>
              </slot>
            </li>
          </template>
        </ul>

        <slot v-if="noOptions" name="nooptions">
          <div :class="classList.noOptions" v-html="noOptionsText"></div>
        </slot>

        <slot v-if="noResults" name="noresults">
          <div :class="classList.noResults" v-html="noResultsText"></div>
        </slot>

        <slot name="afterlist" :options="fo"> </slot>
      </div>
      <slot name="action"></slot>
    </div>

    <!-- Hacky input element to show HTML5 required warning -->
    <input
      v-if="required"
      :class="classList.fakeInput"
      tabindex="-1"
      :value="textValue"
      required
    />

    <!-- Native input support -->
    <template v-if="nativeSupport">
      <input
        v-if="mode == 'single'"
        type="hidden"
        :name="name"
        :value="plainValue !== undefined ? plainValue : ''"
      />
      <template v-else>
        <input
          v-for="(v, i) in plainValue"
          :key="i"
          type="hidden"
          :name="`${name}[]`"
          :value="v"
        />
      </template>
    </template>

    <!-- Create height for empty input -->
    <div :class="classList.spacer"></div>
  </div>
</template>

<script>
import useData from './composables/useData'
import useValue from './composables/useValue'
import useSearch from './composables/useSearch'
import usePointer from './composables/usePointer'
import useOptions from './composables/useOptions'
import usePointerAction from './composables/usePointerAction'
import useDropdown from './composables/useDropdown'
import useMultiselect from './composables/useMultiselect'
import useKeyboard from './composables/useKeyboard'
import useClasses from './composables/useClasses'

export default {
  name: 'BaseMultiselect',
  props: {
    preserveSearch: {
      type: Boolean,
      default: false,
    },
    initialSearch: {
      type: String,
      default: null,
    },
    contentLoading: {
      type: Boolean,
      default: false,
    },
    value: {
      required: false,
    },
    modelValue: {
      required: false,
    },
    options: {
      type: [Array, Object, Function],
      required: false,
      default: () => [],
    },
    id: {
      type: [String, Number],
      required: false,
    },
    name: {
      type: [String, Number],
      required: false,
      default: 'multiselect',
    },
    disabled: {
      type: Boolean,
      required: false,
      default: false,
    },
    label: {
      type: String,
      required: false,
      default: 'label',
    },
    trackBy: {
      type: String,
      required: false,
      default: 'label',
    },
    valueProp: {
      type: String,
      required: false,
      default: 'value',
    },
    placeholder: {
      type: String,
      required: false,
      default: null,
    },
    mode: {
      type: String,
      required: false,
      default: 'single', // single|multiple|tags
    },
    searchable: {
      type: Boolean,
      required: false,
      default: false,
    },
    limit: {
      type: Number,
      required: false,
      default: -1,
    },
    hideSelected: {
      type: Boolean,
      required: false,
      default: true,
    },
    createTag: {
      type: Boolean,
      required: false,
      default: false,
    },
    appendNewTag: {
      type: Boolean,
      required: false,
      default: true,
    },
    caret: {
      type: Boolean,
      required: false,
      default: true,
    },
    loading: {
      type: Boolean,
      required: false,
      default: false,
    },
    noOptionsText: {
      type: String,
      required: false,
      default: 'The list is empty',
    },
    noResultsText: {
      type: String,
      required: false,
      default: 'No results found',
    },
    multipleLabel: {
      type: Function,
      required: false,
    },
    object: {
      type: Boolean,
      required: false,
      default: false,
    },
    delay: {
      type: Number,
      required: false,
      default: -1,
    },
    minChars: {
      type: Number,
      required: false,
      default: 0,
    },
    resolveOnLoad: {
      type: Boolean,
      required: false,
      default: true,
    },
    filterResults: {
      type: Boolean,
      required: false,
      default: true,
    },
    clearOnSearch: {
      type: Boolean,
      required: false,
      default: false,
    },
    clearOnSelect: {
      type: Boolean,
      required: false,
      default: true,
    },
    canDeselect: {
      type: Boolean,
      required: false,
      default: true,
    },
    canClear: {
      type: Boolean,
      required: false,
      default: false,
    },
    max: {
      type: Number,
      required: false,
      default: -1,
    },
    showOptions: {
      type: Boolean,
      required: false,
      default: true,
    },
    addTagOn: {
      type: Array,
      required: false,
      default: () => ['enter'],
    },
    required: {
      type: Boolean,
      required: false,
      default: false,
    },
    openDirection: {
      type: String,
      required: false,
      default: 'bottom',
    },
    nativeSupport: {
      type: Boolean,
      required: false,
      default: false,
    },
    invalid: {
      type: Boolean,
      required: false,
      default: false,
    },
    classes: {
      type: Object,
      required: false,
      default: () => ({
        container:
          'p-0 relative mx-auto w-full flex items-center justify-end box-border cursor-pointer border border-gray-200 rounded-md bg-white text-sm leading-snug outline-none max-h-10',
        containerDisabled:
          'cursor-default bg-gray-200 bg-opacity-50 !text-gray-400',
        containerOpen: '',
        containerOpenTop: '',
        containerActive: 'ring-1 ring-primary-400 border-primary-400',
        containerInvalid:
          'border-red-400 ring-red-400 focus:ring-red-400 focus:border-red-400',
        containerInvalidActive: 'ring-1 border-red-400 ring-red-400',
        singleLabel:
          'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5',
        multipleLabel:
          'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5',
        search:
          'w-full absolute inset-0 outline-none appearance-none box-border border-0 text-sm font-sans bg-white rounded-md pl-3.5',
        tags: 'grow shrink flex flex-wrap mt-1 pl-2',
        tag: 'bg-primary-500 text-white text-sm font-semibold py-0.5 pl-2 rounded mr-1 mb-1 flex items-center whitespace-nowrap',
        tagDisabled: 'pr-2 !bg-gray-400 text-white',
        tagRemove:
          'flex items-center justify-center p-1 mx-0.5 rounded-sm hover:bg-black hover:bg-opacity-10 group',
        tagRemoveIcon:
          'bg-multiselect-remove text-white bg-center bg-no-repeat opacity-30 inline-block w-3 h-3 group-hover:opacity-60',
        tagsSearchWrapper: 'inline-block relative mx-1 mb-1 grow shrink h-full',
        tagsSearch:
          'absolute inset-0 border-0 focus:outline-none !shadow-none !focus:shadow-none appearance-none p-0 text-sm font-sans box-border w-full',
        tagsSearchCopy: 'invisible whitespace-pre-wrap inline-block h-px',
        placeholder:
          'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 text-gray-400 text-sm',
        caret:
          'bg-multiselect-caret bg-center bg-no-repeat w-5 h-5 py-px box-content z-5 relative mr-1 opacity-40 shrink-0 grow-0 transition-transform',
        caretOpen: 'rotate-180 pointer-events-auto',
        clear:
          'pr-3.5 relative z-10 opacity-40 transition duration-300 shrink-0 grow-0 flex hover:opacity-80',
        clearIcon:
          'bg-multiselect-remove bg-center bg-no-repeat w-2.5 h-4 py-px box-content inline-block',
        spinner:
          'bg-multiselect-spinner bg-center bg-no-repeat w-4 h-4 z-10 mr-3.5 animate-spin shrink-0 grow-0',
        dropdown:
          'max-h-60 shadow-lg absolute -left-px -right-px -bottom-1 translate-y-full border border-gray-300 mt-1 overflow-y-auto z-50 bg-white flex flex-col rounded-md',
        dropdownTop:
          '-translate-y-full -top-2 bottom-auto flex-col-reverse rounded-md',
        dropdownHidden: 'hidden',
        options: 'flex flex-col p-0 m-0 list-none',
        optionsTop: 'flex-col-reverse',
        group: 'p-0 m-0',
        groupLabel:
          'flex text-sm box-border items-center justify-start text-left py-1 px-3 font-semibold bg-gray-200 cursor-default leading-normal',
        groupLabelPointable: 'cursor-pointer',
        groupLabelPointed: 'bg-gray-300 text-gray-700',
        groupLabelSelected: 'bg-primary-600 text-white',
        groupLabelDisabled: 'bg-gray-100 text-gray-300 cursor-not-allowed',
        groupLabelSelectedPointed: 'bg-primary-600 text-white opacity-90',
        groupLabelSelectedDisabled:
          'text-primary-100 bg-primary-600 bg-opacity-50 cursor-not-allowed',
        groupOptions: 'p-0 m-0',
        option:
          'flex items-center justify-start box-border text-left cursor-pointer text-sm leading-snug py-2 px-3',
        optionPointed: 'text-gray-800 bg-gray-100',
        optionSelected: 'text-white bg-primary-500',
        optionDisabled: 'text-gray-300 cursor-not-allowed',
        optionSelectedPointed: 'text-white bg-primary-500 opacity-90',
        optionSelectedDisabled:
          'text-primary-100 bg-primary-500 bg-opacity-50 cursor-not-allowed',
        noOptions: 'py-2 px-3 text-gray-600 bg-white',
        noResults: 'py-2 px-3 text-gray-600 bg-white',
        fakeInput:
          'bg-transparent absolute left-0 right-0 -bottom-px w-full h-px border-0 p-0 appearance-none outline-none text-transparent',
        spacer: 'h-9 py-px box-content',
      }),
    },
    strict: {
      type: Boolean,
      required: false,
      default: true,
    },
    closeOnSelect: {
      type: Boolean,
      required: false,
      default: true,
    },
    autocomplete: {
      type: String,
      required: false,
    },
    groups: {
      type: Boolean,
      required: false,
      default: false,
    },
    groupLabel: {
      type: String,
      required: false,
      default: 'label',
    },
    groupOptions: {
      type: String,
      required: false,
      default: 'options',
    },
    groupHideEmpty: {
      type: Boolean,
      required: false,
      default: false,
    },
    groupSelect: {
      type: Boolean,
      required: false,
      default: true,
    },
    inputType: {
      type: String,
      required: false,
      default: 'text',
    },
  },
  emits: [
    'open',
    'close',
    'select',
    'deselect',
    'input',
    'search-change',
    'tag',
    'update:modelValue',
    'change',
    'clear',
  ],
  setup(props, context) {
    const value = useValue(props, context)
    const pointer = usePointer(props, context)
    const dropdown = useDropdown(props, context)
    const search = useSearch(props, context)

    const data = useData(props, context, {
      iv: value.iv,
    })

    const multiselect = useMultiselect(props, context, {
      input: search.input,
      open: dropdown.open,
      close: dropdown.close,
      clearSearch: search.clearSearch,
    })

    const options = useOptions(props, context, {
      ev: value.ev,
      iv: value.iv,
      search: search.search,
      clearSearch: search.clearSearch,
      update: data.update,
      pointer: pointer.pointer,
      clearPointer: pointer.clearPointer,
      blur: multiselect.blur,
      deactivate: multiselect.deactivate,
    })

    const pointerAction = usePointerAction(props, context, {
      fo: options.fo,
      fg: options.fg,
      handleOptionClick: options.handleOptionClick,
      handleGroupClick: options.handleGroupClick,
      search: search.search,
      pointer: pointer.pointer,
      setPointer: pointer.setPointer,
      clearPointer: pointer.clearPointer,
      multiselect: multiselect.multiselect,
    })

    const keyboard = useKeyboard(props, context, {
      iv: value.iv,
      update: data.update,
      search: search.search,
      setPointer: pointer.setPointer,
      selectPointer: pointerAction.selectPointer,
      backwardPointer: pointerAction.backwardPointer,
      forwardPointer: pointerAction.forwardPointer,
      blur: multiselect.blur,
      fo: options.fo,
    })

    const classes = useClasses(props, context, {
      isOpen: dropdown.isOpen,
      isPointed: pointerAction.isPointed,
      canPointGroups: pointerAction.canPointGroups,
      isSelected: options.isSelected,
      isDisabled: options.isDisabled,
      isActive: multiselect.isActive,
      resolving: options.resolving,
      fo: options.fo,
    })

    return {
      ...value,
      ...dropdown,
      ...multiselect,
      ...pointer,
      ...data,
      ...search,
      ...options,
      ...pointerAction,
      ...keyboard,
      ...classes,
    }
  },
}
</script>

<style scoped>
.bg-multiselect-remove {
    background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 320 512' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z'%3e%3c/path%3e%3c/svg%3e")
}
.bg-multiselect-remove-black {
    background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 320 512' fill='black' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z'%3e%3c/path%3e%3c/svg%3e")
}
.bg-multiselect-caret {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' class='h-5 w-5' viewBox='0 0 20 20' fill='currentColor'%3e %3cpath fill-rule='evenodd' d='M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z' clip-rule='evenodd' /%3e %3c/svg%3e")
}
.bg-multiselect-spinner {
    background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 512 512' fill='rgb(var(--color-primary-500))' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M456.433 371.72l-27.79-16.045c-7.192-4.152-10.052-13.136-6.487-20.636 25.82-54.328 23.566-118.602-6.768-171.03-30.265-52.529-84.802-86.621-144.76-91.424C262.35 71.922 256 64.953 256 56.649V24.56c0-9.31 7.916-16.609 17.204-15.96 81.795 5.717 156.412 51.902 197.611 123.408 41.301 71.385 43.99 159.096 8.042 232.792-4.082 8.369-14.361 11.575-22.424 6.92z'%3e%3c/path%3e%3c/svg%3e")
}
</style>
